钱文翔的博客

(翻译)For Range Semantics

原文是《For Range Semantics》

序章

为了更好的理解这篇文章中展现的内容,你应该先阅读以下文章:

四篇文章的索引:

  1. Language Mechanics On Stacks And Pointers

  2. Language Mechanics On Escape Analysis

  3. Language Mechanics On Memory Profiling

  4. Design Philosophy On Data And Semantics

值的概念和指针的语义在 Go 中无处不在。正如之前的文章中说的那样,语义一致性对于完整性和可读性至关重要。它让开发人员,随着代码库的增长,保持一个强大的代码库的心理模型。它可以尽可能减少错误,副作用和未知的表现。

引言

这这篇文章中,我将会探索在 Go 中 for range 语法块同时提供值和指针两种语义形式。我会教你语义的使用并展示给你这个语义如何衍生至更深层次。然后,我会使用一个简单的例子,来展示使用这些语义会出现的错误。

语言技巧

用这段代码来展示 for range 循环的 value 语义格式。

https://play.golang.org/p/_CWCAF6ge3

Listing1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
01 package main
02
03 import "fmt"
04
05 type user struct {
06 name string
07 email string
08 }
09
10 func main() {
11 users := []user{
12 {"Bill", "bill@email.com"},
13 {"Lisa", "lisa@email.com"},
14 {"Nancy", "nancy@email.com"},
15 {"Paul", "paul@email.com"},
16 }
17
18 for i, u := range users {
19 fmt.Println(i, u)
20 }
21 }

在图 1 中,程序声明了 user 类型,创建了四个 user 值并且列出每一个 user 的信息。18 行的 for range 循环使用 value 语义。因为在每一个迭代中,创建切片中的原始的 user 值的拷贝,并在循环中操作这个拷贝值。实际上,调用 Println 创建了循环体中的第二个拷贝。如果要将值语义用于 user 值,这就是你想要的。

如果你想要使用指针,那么 for range 循环将会是这样。

Listing 2

1
2
3
18     for i := range users {
19 fmt.Println(i, users[i])
20 }

现在循环已经修改成指针语义。循环中的代码不再操作它自己的复制,而是操作存在切片中的原始的 user 值。但是调用 Println 仍然使用值语义,传递的是一个拷贝。

为了修正这个需求,我们最后修改一下。

Listing 3

1
2
3
18     for i := range users {
19 fmt.Println(i, &users[i])
20 }

现在就是始终使用的 user 数据的指针技巧了。

Listing4 一步步展示了值和指针语义作为参考

Listing 4

1
2
3
4
       // Value semantics.           // Pointer semantics.
18 for i, u := range users { for i := range users {
19 fmt.Println(i, u) fmt.Println(i, &users[i])
20 } }

更深层次的技巧

比这更深层次的语言技巧。让我们看一下下面 listing5 的程序。这段程序初始化了字符串数组,迭代这些字符串并且每次迭代的时候都改变 index 为 1 的字符串的值。

Listing 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
01 package main
02
03 import "fmt"
04
05 func main() {
06 five := [5]string{"Annie", "Betty", "Charley", "Doug", "Edward"}
07 fmt.Printf("Bfr[%s] : ", five[1])
08
09 for i := range five {
10 five[1] = "Jack"
11
12 if i == 1 {
13 fmt.Printf("Aft[%s]\n", five[1])
14 }
15 }
16 }

这段程序将会输出什么呢?

Listing 6

1
Bfr[Betty] : Aft[Jack]

正如你所希望的那样,第 10 行代码已经改变了 index 为 1 的字符串的值,你可以从输出的结果中看到。这段代码使用了 for range 的指针语义版本。下面的代码将会使用 for range 循环的值语义版本。

https://play.golang.org/p/opSsIGtNU1

Listing 7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
01 package main
02
03 import "fmt"
04
05 func main() {
06 five := [5]string{"Annie", "Betty", "Charley", "Doug", "Edward"}
07 fmt.Printf("Bfr[%s] : ", five[1])
08
09 for i, v := range five {
10 five[1] = "Jack"
11
12 if i == 1 {
13 fmt.Printf("v[%s]\n", v)
14 }
15 }
16 }

在每一个循环的迭代中,代码再次改变了 index 1 的字符串的值,但是这次输出的结果却不一样。

Listing 8

1
Bfr[Betty] : v[Betty]

你可以看到这次 for range 确实使用了值语义。for range 迭代了它自己的对于数组的拷贝。这就是为什么改变的内容在输出的结果中看不到。

当使用值语义格式迭代切片的时候,就会得到切片的头部的拷贝。这就是为什么我 listing 9 不会产生 panic 的原因。

https://play.golang.org/p/OXhdsneBec

Listing 9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
01 package main
02
03 import "fmt"
04
05 func main() {
06 five := []string{"Annie", "Betty", "Charley", "Doug", "Edward"}
07
08 for _, v := range five {
09 five = five[:2]
10 fmt.Printf("v[%s]\n", v)
11 }
12 }

Output:
v[Annie]
v[Betty]
v[Charley]
v[Doug]
v[Edward]

如果你想到 09 行你就会知道,虽然切片在循环中已经被缩短到长度为 2,但是循环值操作属于它自己的切片的拷贝。这让循环可以使用原先长度来迭代因为备份的数组仍是完整的。

如果代码使用 for range 的指针语义格式,那么这个代码会产生 panic。

https://play.golang.org/p/k5a73PHaka

Listing 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
01 package main
02
03 import "fmt"
04
05 func main() {
06 five := []string{"Annie", "Betty", "Charley", "Doug", "Edward"}
07
08 for i := range five {
09 five = five[:2]
10 fmt.Printf("v[%s]\n", five[i])
11 }
12 }

Output:
v[Annie]
v[Betty]
panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
/tmp/sandbox688667612/main.go:10 +0x140

for range 在迭代前就获取了切片的长度,但是在循环的时候长度改变了。在第三次迭代的时候,循环体尝试访问与切片长度关联不上元素。

混合语义

下面展示一个完整的错误例子。这段代码混合使用 user 类型的语义,并引起错误。

https://play.golang.org/p/L_WmUkDYFJ

Listing 11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
01 package main
02
03 import "fmt"
04
05 type user struct {
06 name string
07 likes int
08 }
09
10 func (u *user) notify() {
11 fmt.Printf("%s has %d likes\n", u.name, u.likes)
12 }
13
14 func (u *user) addLike() {
15 u.likes++
16 }
17
18 func main() {
19 users := []user{
20 {name: "bill"},
21 {name: "lisa"},
22 }
23
24 for _, u := range users {
25 u.addLike()
26 }
27
28 for _, u := range users {
29 u.notify()
30 }
31 }

这个例子不是很做作。在 05 行声明了 user 类型,选择指针语义实现 user 类型的方法集。在 main 程序中,for range 循环体使用了值语义,并且每一个 user 都使用了 addlock()方法。第二个循环体用来 notify 每一个 user,同样使用了值语义。

Listing 12

1
2
bill has 0 likes
lisa has 0 likes

输出表明没有 like 增加。我一直强调你应该为给定类型选择一个语义,并坚持使用该类型的数据做所有事情。

下面是代码如何注意与 user 类型的指针语义保持一致的方法。

https://play.golang.org/p/GwAnyBNqPz

Listing 13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
01 package main
02
03 import "fmt"
04
05 type user struct {
06 name string
07 likes int
08 }
09
10 func (u *user) notify() {
11 fmt.Printf("%s has %d likes\n", u.name, u.likes)
12 }
13
14 func (u *user) addLike() {
15 u.likes++
16 }
17
18 func main() {
19 users := []user{
20 {name: "bill"},
21 {name: "lisa"},
22 }
23
24 for i := range users {
25 users[i].addLike()
26 }
27
28 for i := range users {
29 users[i].notify()
30 }
31 }

// Output:
bill has 1 likes
lisa has 1 likes

总结

值和指针语义是 Go 语言编程的很大一部分,正如我所展示的,整合进 for range 循环中。当使用 for range 循环的时候,确定你使用了你迭代的给定类型的正确的格式。尽量不要混合使用语义,如果你不注意的话,使用 for range 的时候很容易这么做。

语言赋予你这种选择语义的能力,并能始终如一地工作。这是你想要充分利用的东西。我想让你决定每种类型使用什么语义并保持一致。你对一个数据的语义越一致,您的代码库就会越好。如果您有一个很好的理由来改变语义,那么就将其广泛地记录下来。